package mk.jug.academy.service;

import mk.jug.academy.exception.BusinessException;
import mk.jug.academy.model.Course;
import mk.jug.academy.model.Page;
import mk.jug.academy.model.Student;
import mk.jug.academy.model.status.StudentStatus;
import mk.jug.academy.preconditions.ErrorCodes;
import mk.jug.academy.preconditions.PreconditionExceptions;
import mk.jug.academy.testing.bdd.IntegrationBehavior;
import mk.jug.academy.testing.factory.CourseTestFactory;
import mk.jug.academy.testing.factory.StudentTestFactory;
import mk.jug.academy.testing.util.TestHelper;

import org.springframework.beans.factory.annotation.Autowired;

import com.googlecode.instinct.marker.annotate.Specification;
import com.googlecode.instinct.marker.annotate.Specification.SpecificationState;
import com.googlecode.instinct.marker.annotate.Subject;

/**
 * An integration test covering the behavior exhibited regarding the
 * {@link Student} management process.
 *
 * @author Ice.Penov
 */
public class StudentManagement extends IntegrationBehavior {
	
	@Autowired private CourseService courseService;
	@Autowired private StudentService studentService;
	
	@Subject private Course course1;
	@Subject private Course course2;
	
	@Subject private Student student1;
	@Subject private Student student2;
	@Subject private Student student3;
	
	@Override
	protected void beforeHook() {
		TestHelper.init(courseService);
		TestHelper.init(studentService);
		
		course1 = CourseTestFactory.createCourse();
		course2 = CourseTestFactory.createCourse();
		
		student1 = StudentTestFactory.createStudent();
		student2 = StudentTestFactory.createStudent();
		student3 = StudentTestFactory.createStudent();
	}
	
	/**
	 * User action:
	 * An {@link Student} is being assigned to a {@link Course} not in the AVAILABLE state.
	 * 
	 * Expected behavior:
	 * 1. Throw an {@link BusinessException} with an appropriate error code.
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void failToAssignStudentIfCourseIsNotInAvailableState() {
		courseService.submitNewCourse(course1);
		studentService.registerStudent(student1);
		studentService.approveRegistration(student1);
		//assign to a hatching course.
		new ExceptionExpectation() {
			public void execute() {
				courseService.assignStudent(course1, student1);
			}
		}.checkForErrorCode(BusinessException.class, ErrorCodes.ERROR_COURSE_STUDENT_ASSIGN);
		courseService.hatchCourse(course1);
		courseService.startCourse(course1);
		//assign to an already running course.
		new ExceptionExpectation() {
			public void execute() {
				courseService.assignStudent(course1, student1);
			}
		}.checkForErrorCode(BusinessException.class, ErrorCodes.ERROR_COURSE_STUDENT_ASSIGN);
		courseService.finishCourse(course1);
		courseService.deactivateCourse(course1);
		//assign to a de-activated course
		new ExceptionExpectation() {
			public void execute() {
				courseService.assignStudent(course1, student1);
			}
		}.checkForErrorCode(BusinessException.class, ErrorCodes.ERROR_COURSE_STUDENT_ASSIGN);
	}
	
	/**
	 * User action:
	 * A {@link Student} not in the WAITING_FOR_ENROLLMENT or ENROLLED state is being assigned to e {@link Course}.
	 * 
	 * Expected behavior:
	 * 1. Throw an {@link BusinessException} with the appropriate error code.
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void failToAssignStudentToCourseIfStudentIsNotInWaitingForEnrollmentState() {
		courseService.submitNewCourse(course1);
		courseService.hatchCourse(course1);
		studentService.registerStudent(student1);
		//assign SIGNED_UP student
		new ExceptionExpectation() {
			public void execute() {
				courseService.assignStudent(course1, student1);
			}
		}.checkForErrorCode(BusinessException.class, ErrorCodes.ERROR_STUDENT_WAITING_FOR_ENROLLMENT_STATE);
		studentService.approveRegistration(student1);
		student1.setStatus(StudentStatus.REJECTED);
		//assign REJECTED student
		new ExceptionExpectation() {
			public void execute() {
				courseService.assignStudent(course1, student1);
			}
		}.checkForErrorCode(BusinessException.class, ErrorCodes.ERROR_STUDENT_WAITING_FOR_ENROLLMENT_STATE);
	}
	
	/**
	 * User action:
	 * Assign a non-persisted {@link Student} to an existing {@link Course}.
	 * 
	 * Expected behavior:
	 * 1. Throw an {@link IllegalArgumentException} with the appropriate message.
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void failToAssignStudentToCourseIfStudentIsNotSaved() {
		courseService.submitNewCourse(course1);
		courseService.hatchCourse(course1);
		new ExceptionExpectation() {
			public void execute() {
				courseService.assignStudent(course1, student1);
			}
		}.checkFor(IllegalArgumentException.class, PreconditionExceptions.ID_IS_NULL);
	}
	
	/**
	 * User action:
	 * Assign a {@link Student} to a {@link Course}.
	 * 
	 * Expected behavior:
	 * 1. The {@link Course} should list that {@link Student}
	 * 2. The {@link Student} should list this {@link Course}
	 * 3. The {@link Student} should be in the ENROLLED pool
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void enrollStudentToCourse() {
		//initialize.
		TestHelper.prepareAvailableCourse(course1, course2);
		TestHelper.prepareActiveStudent(student1);
		//make the calls.
		courseService.assignStudent(course1, student1);
		//expectations.
		expect.that(courseService.fetchStudentsForCourse(course1)).isOfSize(1);
		expect.that(courseService.fetchStudentsForCourse(course1)).containsItem(student1);
		//all of them should be ENROLLED:
		expect.that(studentService.listEnrolled(1, 2).getItems()).containsItems(student1);
	}
	
	/**
	 * User action:
	 * Remove a {@link Student} from a {@link Course}.
	 * 
	 * Expected behavior:
	 * 1. The {@link Course} should no longer list the {@link Student}
	 * 2. The {@link Student} should no longer list the {@link Course}
	 * 3. If the removed {@link Course} was the only one for the {@link Student},
	 * it's status should go back to {@link StudentStatus#WAITING_FOR_ENROLLMENT}
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void removeStudentFromCourse() {
		//initialize.
		TestHelper.prepareAvailableCourse(course1, course2);
		TestHelper.prepareActiveStudent(student1, student2, student3);
		//make the assignments.
		courseService.assignStudent(course1, student1);
		//now, remove it.
		courseService.removeStudent(course1, student1);
		//expect some behavior.
		expect.that(courseService.fetchStudentsForCourse(course1)).isEmpty();
		//expectations about the state:
		expect.that(studentService.listWaitingForEnrollment(1, 2).getItems()).containsItem(student1);
	}
	
	/**
	 * User action:
	 * Edit the {@link Student}s properties.
	 * 
	 * Expected behavior:
	 * 1. All changes to be saved.
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void editStudentProperties() {
		TestHelper.prepareActiveStudent(student1);
		student1.setBio("Changed");
		student1.setFirstName("Changed");
		student1.setLastName("Changed");
		student1.setEmail("Changed");
		studentService.saveChanges(student1);
		Student loaded = studentService.load(student1.getId());
		expect.that(loaded.getBio()).isEqualTo(student1.getBio());
		expect.that(loaded.getFirstName()).isEqualTo(student1.getFirstName());
		expect.that(loaded.getLastName()).isEqualTo(student1.getLastName());
		expect.that(loaded.getEmail()).isEqualTo(student1.getEmail());
	}
	
	/**
	 * User action:
	 * Fetch the students in a paed manner.
	 * 
	 * Expected behavior:
	 * List the records as expected.
	 */
	@Specification(state = SpecificationState.COMPLETE)
	public void navigateStudentsUsingPaging() {
		student1.setRating(1);
		student2.setRating(2);
		student3.setRating(3);
		TestHelper.prepareActiveStudent(student1, student2, student3);
		//open the pages.
		Page<Student> page1 = studentService.listAll(1, 2);
		Page<Student> page2 = studentService.listAll(2, 2);
		Page<Student> page3 = studentService.listAll(3, 2);
		//expectations.
		//size:
		expect.that(page1.getTotalCount()).isEqualTo(3);
		expect.that(page2.getTotalCount()).isEqualTo(3);
		expect.that(page3.getTotalCount()).isEqualTo(3);
		expect.that(page1.getItems()).isOfSize(2);
		expect.that(page2.getItems()).isOfSize(1);
		expect.that(page3.getItems()).isEmpty();
		//check contents of the pages:
		expect.that(page1.getItems()).containsItems(student3, student2);
		expect.that(page2.getItems()).containsItem(student1);
	}

}